#! /usr/bin/env python
# -*- coding: utf-8 -*-
import os
import random
import re
import time
from typing import Literal

import pandas as pd
from playwright.sync_api import Page, Locator
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

from .locate_by import By
from .logger import logger
from .screenshot import take_screenshot


class BasePage:

    def __init__(self, page: Page):
        self.page = page

        self.FIELD_MAPPING = {}
        self.SELECT_FIELDS = {}
        self.DATE_FIELDS = {}
        self.FILE_FIELDS = {}

    def navigate(self, url):
        """
        打开页面
        :param url: 页面地址
        :return:
        """
        logger.info(f"进入页面：{url}")
        self.page.goto(url)

    def switch_menu(self, menu_name, **kwargs):
        """
        切换菜单，菜单的标准role是menuitem
        :param menu_name: 菜单名称
        :return:
        """
        logger.info(f"菜单切换到：{menu_name}")
        self.page.get_by_role(By.menuitem, name=menu_name, **kwargs).click()
        # 显示等待0.5秒，防止点击菜单太快，部分页面加载太慢。虽然点击了菜单，实际显示的页面还是上个菜单的页面
        time.sleep(0.5)

    def click(self, loc: dict, **kwargs):
        """
        点击元素，可实现各种按钮的点击
        :param loc: 元素的属性
        :return:
        """
        if loc.__contains__('page_role'):
            if loc.__contains__('seq'):
                page_role = loc["page_role"]
                page_title = loc["page_title"]
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                seq = loc["seq"]
                logger.info(f"在{page_title}页面，通过role点击第{seq}个元素: {loc_role} - {loc_name}")
                self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1).click()
            else:
                page_role = loc["page_role"]
                page_title = loc["page_title"]
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                logger.info(f"在{page_title}页面，通过role点击元素: {loc_role} - {loc_name}")
                self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs).click()
        else:
            if loc.__contains__('seq'):
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                seq = loc["seq"]
                logger.info(f"通过role点击第{seq}个元素: {loc_role} - {loc_name}")
                self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1).click()
            else:
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                logger.info(f"通过role点击元素: {loc_role} - {loc_name}")
                self.page.get_by_role(loc_role, name=loc_name, **kwargs).click()

    def fill(self, loc: dict, text='', **kwargs):
        """
        输入内容
        :param loc: 元素的属性
        :param text: 输入的内容，默认输入空字符串
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role定位元素: {loc_role} - {loc_name}，并输入{text}")
            self.page.get_by_role(page_role, name=page_title, **kwargs). \
                get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).fill(text)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role定位元素: {loc_role} - {loc_name}，并输入{text}")
            self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).fill(text)

    def fill_number(self, loc: dict, num: int, **kwargs):
        """
        通过键盘输入整数，处理一些职只能输入数值的组件，如spinbutton
        :param loc: 元素的属性
        :param num: 待输入的整数
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role定位元素: {loc_role} - {loc_name}，并输入{num}")
            for i in str(num):
                # 通过键盘输入数字，键盘上的数字0对应Digit0
                self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).press('Digit' + i)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role定位元素: {loc_role} - {loc_name}，并输入{num}")
            for i in str(num):
                self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).press('Digit' + i)

    def clear(self, loc: dict, **kwargs):
        """
        清空元素
        :param loc: 元素的属性
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role清空元素: {loc_role} - {loc_name}")
            self.page.get_by_role(page_role, name=page_title, **kwargs). \
                get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).clear()
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role清空元素: {loc_role} - {loc_name}")
            self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).clear()

    def clear_by_backspace(self, loc: dict, **kwargs):
        """
        通过键盘的backspace键清空元素，可处理一些被禁用的输入框（无法使用clear操作的情况）
        :param loc: 元素的属性
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role清空元素: {loc_role} - {loc_name}")
            # 用于处理一些被警用的输入组件，如spinbutton等，此处使用ctrl+a组合键先选中，然后再按下backspace进行清除
            self.page.get_by_role(page_role, name=page_title, **kwargs).get_by_role(loc_role, name=loc_name, **kwargs). \
                nth(seq).press('Control+KeyA')
            self.page.get_by_role(page_role, name=page_title, **kwargs).get_by_role(loc_role, name=loc_name, **kwargs). \
                nth(seq).press('Backspace')

        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role清空元素: {loc_role} - {loc_name}")
            self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).press('Control+KeyA')
            self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).press('Backspace')

    def check(self, loc: dict, **kwargs):
        """
        通过文本描述勾选单选或复选框
        :param loc: 元素的属性
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role定位元素: {loc_role} - {loc_name}，并勾选{loc_name}")
            if loc_role == By.checkbox:
                # 如果类型是checkbox，不能用force click方式。只能通过点击checkbox的文本
                self.page.get_by_role(page_role, name=page_title, **kwargs).get_by_text(loc_name, exact=True). \
                    nth(seq).click()
            else:
                # 如果是radio，可以通过force click方式点击
                self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).click(force=True)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role定位元素: {loc_role} - {loc_name}，并勾选{loc_name}")
            if loc_role == By.checkbox:
                self.page.get_by_text(loc_name, exact=True).nth(seq).click(force=True)
            else:
                self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq).click(force=True)

    def check_complex(self, loc: dict, text, **kwargs):
        """
        处理包含child属性的复杂定位器，用于勾选单选或复选框
        :param loc: 元素的属性（包含child属性）
        :param text: 勾选项的文本描述
        :return:
        """         
        if loc.__contains__('child'):
            # 处理包含child属性的复杂定位器
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过复杂定位器定位元素: {loc_role} - {loc_name}，并勾选{text}")
            
            # 先定位到radiogroup
            radiogroup = self.page.get_by_role(loc_role, name=loc_name, **kwargs)
            
            # 在radiogroup中查找包含指定文本的单选框
            radio_option = radiogroup.get_by_text(text, exact=True)
            radio_option.click()
            
        else:   
            pass
            
    
    def select_row_checkbox(self, target_value=None):
        """
        勾选数据表格的复选框

        参数:
            target_value: str, 根据这个值找数据所在行，尽量唯一
                          int, 勾选对应的行，比如勾选第一行就穿1，勾选所有就传0
                          list, 勾选多行，比如勾选第一行和第二行，传[1, 2]
        """
        # 定位目标行
        if isinstance(target_value, str):
            # 如果有重复的，勾选第一个
            target_row = self.page.get_by_role('rowgroup').get_by_role("row").filter(has_text=target_value).nth(0)
            # target_row.locator("label span").nth(1).click()
            target_row.get_by_role('cell').nth(0).click()
        if isinstance(target_value, int):
            self.page.get_by_role('rowgroup').get_by_role('row').nth(target_value).get_by_role('cell').nth(0).click()
        if isinstance(target_value, list):
            for value in target_value:
                self.page.get_by_role('rowgroup').get_by_role('row').nth(value).get_by_role('cell').nth(0).click()
        if target_value is None:
            # 不处置相当于不勾选，可用于测试不勾选数据直接点击删除等操作
            pass

    def select(self, loc: dict, text=None, **kwargs):
        """
        处理下拉选项，先点击下拉框，然后再从option里面选择一个值。如果不提供选项的值就会随机选择一个
        :param loc: 元素的属性
        :param text: 待选项的值，如果不提供则随机选择一个。如果传入空字符串，则不进行选择操作
        :return:
        """
        if text == "":
            return
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role定位元素: {loc_role} - {loc_name}")
            # 这里要先点击combobox，然后才能从option里面进行选择，option是动态加载的
            # 这里把click方法换成用press('Enter')，可以防止select默认有选项的情况下点击失效
            self.page.get_by_role(page_role, name=page_title, **kwargs). \
                get_by_role(loc_role, name=loc_name, **kwargs).press('Enter')
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role定位元素: {loc_role} - {loc_name}")
            # 这里要先点击combobox，然后才能从option里面进行选择，option是动态加载的
            self.page.get_by_role(loc_role, name=loc_name, **kwargs).press('Enter')
        # 获取下拉框里面的所有选项
        all_contents = self.page.get_by_role('option').all_text_contents()
        if text is None and all_contents.__len__() != 0:
            # 随机选择一个选项
            inx = random.randint(0, all_contents.__len__() - 1)
            logger.info(f"选择选项：{all_contents[inx]}")
            self.page.get_by_role('option', name=all_contents[inx], exact=True).click()
        else:
            # 如果loc里面包含seq，说明同一个页面不同的下拉框里面出现了相同的选项
            if loc.__contains__('seq'):
                seq = loc["seq"]
                logger.info(f"选择第{seq}个选项：{text}")
                self.page.get_by_role('option', name=text, exact=True).nth(seq - 1).click()
            else:
                logger.info(f"选择选项：{text}")
                self.page.get_by_role('option', name=text, exact=True).click()

    def fill_date_time(self, loc: dict, *args, **kwargs):
        """
        用于填写级联的开始和结束时间，先定位group，然后再根据loc里面的child输入时间
        :param loc: loc: 元素属性
        :return:
        eg: 调用方式：fill_date_time(loc, "2025-09-11 12:12:12", "2025-09-12 12:12:12")
        如果是输入单个日期或时间，可以先click，然后再调用fill
        """
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"在{page_title}页面，通过role定位元素: {loc_role} - {loc_name}，并选择输入时间")
            for arg, loc_time in zip(args, loc['child']):
                # 依次填入时间
                logger.info(f'输入{loc_time["loc_name"]}: {arg}')
                self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs). \
                    get_by_role(loc_time["loc_role"], name=loc_time["loc_name"], **kwargs).fill(arg)
            # 这里暂停一秒，是为了解决列表按时间查询的问题。有可能默认进入页面时的列表查询比自定义查询慢，就会导致页面显示的默认查询的结果
            time.sleep(1)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            logger.info(f"通过role定位元素: {loc_role} - {loc_name}，并选择输入时间")
            for arg, loc_time in zip(args, loc['child']):
                # 依次填入时间
                logger.info(f'输入{loc_time["loc_name"]}: {arg}')
                self.page.get_by_role(loc_role, name=loc_name, **kwargs). \
                    get_by_role(loc_time["loc_role"], name=loc_time["loc_name"], **kwargs).fill(arg)
            time.sleep(1)

    # 截屏并保存
    def take_screenshot(self, name):
        take_screenshot(self.page, name)

    def wait_for_field_autofill(self, locator_info: dict, timeout: int = 10000):
        """
        等待字段自动填充值（处理新增和编辑的三种场景）

        参数:
            locator_info: 包含定位信息的字典
            timeout: 超时时间(毫秒)，默认10s
        """
        field_name = locator_info["loc_name"]
        logger.info(f'等待字段 [{field_name}] 自动填充值')

        # 获取定位元素
        element = self.page.get_by_role(locator_info["loc_role"], name=field_name, exact=True)

        # 等待元素可见
        try:
            element.wait_for(state="visible", timeout=timeout)
        except PlaywrightTimeoutError:
            logger.warning(f"字段 [{field_name}] 在 {timeout}ms 内未变为可见")
            self.take_screenshot(f"field_not_visible_{field_name}")
            raise

        # 获取初始值
        initial_value = element.input_value()
        logger.debug(f"字段 [{field_name}] 初始值: '{initial_value}'")

        # 轮询检查值变化
        start_time = time.time()
        poll_interval = 300  # 毫秒
        filled = False

        while (time.time() - start_time) * 1000 < timeout:
            current_value = element.input_value()

            # 情况1: 新增场景 - 从空变为非空
            if not initial_value.strip() and current_value.strip():
                logger.info(f'检测到字段值已填充: "{current_value}"')
                filled = True
                break

            # 情况2: 编辑场景 - 值发生变化
            if initial_value.strip() and current_value != initial_value:
                logger.info(f'检测到字段值已更新: "{initial_value}" → "{current_value}"')
                filled = True
                break

            # 情况3: 编辑场景 - 值没有变化
            # 额外等待确保填充操作完成
            self.page.wait_for_timeout(poll_interval)

        # 最终检查（处理轮询期间未检测到变化的情况）
        final_value = element.input_value()

        # 情况1: 新增场景最终检查
        if not filled and not initial_value.strip() and final_value.strip():
            logger.info(f'字段 [{field_name}] 最终已填充: "{final_value}"')
            filled = True

        # 情况2/3: 编辑场景最终检查
        if not filled and initial_value.strip():
            # 值发生变化
            if final_value != initial_value:
                logger.info(f'字段 [{field_name}] 最终已更新: "{initial_value}" → "{final_value}"')
                filled = True
            # 值没有变化（系统重新填充相同值）
            elif final_value.strip():
                logger.info(f'字段 [{field_name}] 保持原值: "{final_value}" (编辑场景)')
                filled = True

        # 成功返回最终值
        if filled:
            return final_value

        # 失败处理（最终值仍为空）
        error_msg = (
            f"字段 [{field_name}] 在 {timeout}ms 内未自动填充\n"
            f"初始值: '{initial_value}'\n"
            f"最终值: '{final_value}'"
        )
        logger.error(error_msg)
        self.take_screenshot(f"field_autofill_timeout_{field_name}")
        raise PlaywrightTimeoutError(error_msg)

    def wait_for_message(self, message: str, timeout: int = 30000):
        """
        等待提示消息出现

        参数:
            message: 预期显示的消息文本
            timeout: 超时时间(毫秒)
        """
        logger.info(f'等待消息弹窗: "{message}"')

        try:
            success_toast = self.page.get_by_text(message)
            success_toast.wait_for(state="visible", timeout=timeout)

            logger.info(f'消息弹窗已显示: "{message}"')
            return True

        except Exception as e:
            logger.error(f'等待消息超时: {e}')
            raise TimeoutError(f'未检测到提示消息: "{message}"')

    def upload_file(self, file_path, seq: int = None):
        """
        用于上传文件，此处使用上传组件的css定位，如果上传失败可能是非通用上传组件
        :param file_path: 文件地址，上传单个文件传字符串，多个文件传列表。如：['111.pgn', '222.png']
        :param seq: 处理同一页面有多个上传组件，通过顺序定位，从1开始
        :return:
        """
        try:
            if seq is not None:
                logger.info(f'上传文件： {file_path}')
                self.page.locator('.uploader-btn').nth(seq).set_input_files(file_path)
            else:
                logger.info(f'上传文件： {file_path}')
                self.page.locator('.uploader-btn').set_input_files(file_path)
        except Exception as err:
            logger.error('上传文件失败')
            raise err

    def get_loc_value(self, loc: dict, **kwargs):
        """
        获取元素里面填充的值，比如下拉框，或输入框。主要针对查看页面
        :param loc: 注意：如果类型是combobox，必须要传入seq，从1开始。防止页面有多个被禁用的combobox
        :param kwargs:
        :return:
        """
        value = None
        if loc.__contains__('page_role'):
            if loc.__contains__('seq'):
                page_role = loc["page_role"]
                page_title = loc["page_title"]
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                seq = loc["seq"]
                logger.info(f"在{page_title}页面，通过role获取第{seq}个元素的值: {loc_role} - {loc_name}")
                # 首先要能找到元素，然后再去取值。防止取错
                if self.page.get_by_role(page_role, name=page_title, **kwargs). \
                        get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1) and loc_role == By.combobox:
                    # 如果是被禁用的combobox，通过className去取span的值
                    value = self.page.get_by_role(page_role, name=page_title, **kwargs). \
                        locator('.el-select__selected-item.el-select__placeholder > span').nth(seq - 1).text_content()
                else:
                    value = self.page.get_by_role(page_role, name=page_title, **kwargs). \
                        get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1).input_value()
            else:
                page_role = loc["page_role"]
                page_title = loc["page_title"]
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                logger.info(f"在{page_title}页面，通过role获取元素的值: {loc_role} - {loc_name}")
                # 非禁用的combobox，可以直接使用input_value取值
                value = self.page.get_by_role(page_role, name=page_title, **kwargs). \
                    get_by_role(loc_role, name=loc_name, **kwargs).input_value()
        else:
            if loc.__contains__('seq'):
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                seq = loc["seq"]
                logger.info(f"通过role获取第{seq}个元素的值: {loc_role} - {loc_name}")
                if self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1) and loc_role == By.combobox:
                    value = self.page.locator('.el-select__selected-item.el-select__placeholder > span'). \
                        nth(seq - 1).text_content()
                else:
                    value = self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq - 1).input_value()
            else:
                loc_role = loc["loc_role"]
                loc_name = loc["loc_name"]
                logger.info(f"通过role获取元素: {loc_role} - {loc_name}")
                value = self.page.get_by_role(loc_role, name=loc_name, **kwargs).input_value()
        return value

    def select_single_tree(self, loc: dict, value: str, level_flag=False):
        """
        树结构展开单选

        参数:
            loc: 定位器字典
            value: 目标字符串
            level_flag:
                True - 选择完整路径："重庆市/重庆城区/潼南区"，选择"重庆市/重庆城区/潼南区"
                False - 默认False只选最后一级：“化工产业园区/测试单位机构/测试部门”，只选"测试部门"
        """
        if not value:
            raise ValueError("值不能为空")

        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=page_role, name=page_title).get_by_role(role=loc_role, name=loc_name)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=loc_role, name=loc_name)
        # 点击展开选择框
        combobox.press('Enter')

        # 逐级展开树
        levels = value.split('/')
        # current_path = ""
        # for level in levels[:-1]:
        #     current_path = f"{current_path}/{level}" if current_path else level
        #     self.page.get_by_role("treeitem", name=current_path).locator("i").click()
        #
        # # 选择最终目标
        # target_value = value if level_flag else levels[-1]
        # logger.info(f"选择选项：{target_value}")
        # self.page.get_by_role('option', name=target_value, exact=True).click()
        # 上面定位i标签，可能会因为多个i标签导致定位失败，改成使用选择option
        if level_flag is False:
            for option in levels:
                self.page.get_by_role(By.tooltip, name='').get_by_role(By.option, name=option).click()
        else:
            # 有两种情况，1、选择区域（比如新增应急资源，选择行政区域），需要把每一层拼在一起 2、不需要拼装，每一层都可以直接选择
            option_num = len(levels)
            join_option = ''
            for index, option in enumerate(levels):
                if index == option_num - 1:
                    self.page.get_by_role(By.tooltip, name='').get_by_role(By.option, name=value).click()
                else:
                    # 从0开始一层一层的选择
                    if index == 0:
                        join_option = option
                    else:
                        join_option = join_option + '/' + option
                    try:
                        self.page.get_by_role(By.tooltip, name='').get_by_role('treeitem', name=join_option). \
                            get_by_role('img').click()
                    except:
                        # 如果定位不到join_option，则直接定位当前这一层的内容（就是第2种情况，不需要拼接）
                        self.page.get_by_role(By.tooltip, name='').get_by_role('treeitem', name=option). \
                            get_by_role('img').click()

    def select_multiple_tree(self, loc: dict, values: list):
        """
        树结构展开并多选

        参数：
            loc: 定位器字典
            values: 选值列表，例如：material_use = [{"自然灾害":["台风", "地震"]}, "事故灾难", "其他突发事件"]

        """

        def process_node(node, parent_path=""):
            if isinstance(node, dict):
                for key, children in node.items():
                    full_path = f"{parent_path}/{key}" if parent_path else key
                    self.page.locator("div").filter(has_text=re.compile(f"^{full_path}$")).locator("i").click()
                    process_node(children, full_path)
            elif isinstance(node, list):
                for child in node:
                    process_node(child, parent_path)
            else:
                self.page.get_by_role(role="treeitem", name=node).locator("span").nth(1).click()

        if not isinstance(values, list):
            raise TypeError(f"{values} 必须是列表类型")

        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=page_role, name=page_title).get_by_role(role=loc_role, name=loc_name)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=loc_role, name=loc_name)

        # 点击展开选择框
        combobox.press('Enter')
        # 递归处理所有节点
        for value in values:
            process_node(value)

    def clear_multiple_tree(self, loc: dict, expect_str: str, max_attempts: int = 30, **kwargs):
        """
        树结构多选框内容清除

        参数：
            loc: 定位器字典
            expect_str: 清空后的预期文本，例如“请选择 用途”
            max_attempts: 最大尝试次数,默认30次
        """
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=page_role, name=page_title).get_by_role(role=loc_role, name=loc_name)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            combobox = self.page.get_by_role(role=loc_role, name=loc_name)

        # 点击展开选择框
        combobox.press('Enter')
        # 清除内容，直到显示预期文本，例如“请选择 用途”
        attempts = 0
        while attempts < max_attempts:
            attempts += 1

            # 尝试清除内容
            combobox.press('Backspace')

            # 检查是否已清空,出现预期文本
            loc = self.page.get_by_text(expect_str)
            if loc.is_visible():
                print(f"成功清除选择框内容（尝试 {attempts} 次）")
                return True

        # 清除失败处理
        print(f"警告：清除操作在 {max_attempts} 次尝试后仍未成功")
        return False

    SPECIAL_FIELD_HANDLERS = {}
    @classmethod
    def register_special_field(cls, field_name):
        """装饰器：注册特殊字段处理器"""

        def decorator(handler):
            cls.SPECIAL_FIELD_HANDLERS[field_name] = handler
            return handler

        return decorator

    def fill_form(self, locator_class, data, clear_fields=False):
        """通用表单填充逻辑"""
        for field, value in data.items():
            if value is None or value == '':
                continue

            locator = getattr(locator_class, f"loc_{field}", None)
            if not locator:
                logger.warning(f"未找到字段 '{field}' 的定位器，跳过")
                continue

            logger.info(f"设置字段 [{field}]: {value}")

            # 检查是否有注册的特殊字段处理器
            handler = self.SPECIAL_FIELD_HANDLERS.get(field)
            if handler:
                handler(self, field, value, locator, locator_class, clear_fields)
                continue

            # 标准字段处理
            if field in self.SELECT_FIELDS:
                if isinstance(value, list):
                    if clear_fields:
                        self.clear_multiple_tree(locator, expect_str=locator['expect_str'])
                    self.select_multiple_tree(locator, value)
                else:
                    self.select(locator, value)
            elif field in self.FILE_FIELDS:
                self.upload_file(locator, value)
            elif field in self.DATE_FIELDS:
                # 清空字段（编辑时需要）
                if clear_fields:
                    self.clear(locator)
                self.click(locator)
                self.fill(locator, value)
            else:
                # 清空字段（编辑时需要）
                if clear_fields:
                    try:
                        self.clear(locator)
                    except:
                        # spinbutton类型的要用这个方法清空
                        self.clear_by_backspace(locator)
                self.fill(locator, value)

    def get_value_from_table(self, row: int = 0, column: int = 0, text: str = None):
        """
        1、用于获取页面列表中指定行中指定列的值，注意表头是0行
        2、也可以传列值和一个唯一的文本自动查询出记录所在行
        :param text: 通过text查出记录所在行，然后再根据传入的column取值
        :param row: 所在行，从0开始
        :param column: 所在列，从0开始
        :return:
        """
        # 暂停3s用于等待页面列表里面的状态等值的刷新
        time.sleep(3)
        if text is not None:
            target_row = self.page.get_by_role('rowgroup').get_by_role("row").filter(has_text=text).nth(0)
            return target_row.get_by_role('cell').nth(column).all_text_contents()[0]
        else:
            return self.page.get_by_role('rowgroup').get_by_role('row').nth(row).get_by_role('cell').nth(column). \
                all_text_contents()[0]

    def export_and_download(self, loc: dict, file_type: Literal['word', 'excel', 'pdf', 'pic']):
        """
        用于从浏览器下载文件并保存，或是导出列表内容并保存到excel
        :param loc: 下载按钮或导出按钮的定位
        :param file_type: 下载文件的类型，跟根据文件类型返回读取的内容。主要实现下载excel
        :return: 如果是excel：返回文件的行数和列数，类型是元祖：(行数, 列数)。其他类型返回True(表示下载成功)
        """
        # 下载的文件存放在download目录下面
        download_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "\\download"
        try:
            with self.page.expect_download() as download_info:
                self.click(loc)
            download = download_info.value
            file_name = os.path.join(download_dir, download.suggested_filename)
            download.save_as(file_name)
        except Exception as err:
            logger.error('下载文件失败')
            raise err
        # 读取文件内容
        if file_type == 'excel':
            df = pd.read_excel(file_name, engine='openpyxl')
            # 返回文件的行数和列数，类型是元祖：(行数, 列数)
            return df.shape
        else:
            return True

    def get_locator_by_role(self, loc: dict, **kwargs) -> Locator:
        """
        用于通过传入的loc获取一个locator对象
        :param loc:
        :return:
        """
        if loc.__contains__('seq'):
            seq = loc["seq"]
        else:
            seq = 0
        if loc.__contains__('page_role'):
            page_role = loc["page_role"]
            page_title = loc["page_title"]
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            return self.page.get_by_role(page_role, name=page_title, **kwargs). \
                get_by_role(loc_role, name=loc_name, **kwargs).nth(seq)
        else:
            loc_role = loc["loc_role"]
            loc_name = loc["loc_name"]
            return self.page.get_by_role(loc_role, name=loc_name, **kwargs).nth(seq)

    def close_modal_dialog(self):
        """
        用于直接关闭当前正打开的模态窗口
        :return:
        """
        self.page.get_by_role(By.button, name='关闭此对话框').click()
